// Copyright lowRISC contributors (OpenTitan project).
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
//
// USB Mixed streaming data test
//
// This test requires interaction with the USB DPI model or a test application
// on the USB host. The test initializes the USB device and configures a set of
// endpoints for data streaming using bulk transfers.
//
// The DPI model mimicks a USB host. After device initialization, it detects
// the assertion of the pullup and first assigns an address to the device.
// For this test it will then repeatedly fetch data via IN requests to
// each stream and propagate that data to the corresponding OUT endpoints.
//
// The data itself is pseudo-randomly generated by the sender and,
// independently, by the receiving code to check that the data has been
// propagated unmodified and without data loss, corruption, replication etc.

/*
Thoughts:

  randomize the input and output endpoints for each stream; pick without
  replacement(!)

  randomize the stream types

  randomize the sending, trying, receiving and max_packets
*/

#include "sw/device/lib/dif/dif_pinmux.h"
#include "sw/device/lib/runtime/log.h"
#include "sw/device/lib/runtime/print.h"
#include "sw/device/lib/testing/pinmux_testutils.h"
#include "sw/device/lib/testing/test_framework/check.h"
#include "sw/device/lib/testing/test_framework/ottf_main.h"
#include "sw/device/lib/testing/usb_testutils.h"
#include "sw/device/lib/testing/usb_testutils_controlep.h"
#include "sw/device/lib/testing/usb_testutils_diags.h"
#include "sw/device/lib/testing/usb_testutils_streams.h"

#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"  // Generated.

// Number of streams to be tested
#ifndef NUM_STREAMS
#define NUM_STREAMS USBUTILS_STREAMS_MAX
#endif

#define TRANSFER_BYTES_SILICON (4U << 20)
#define TRANSFER_BYTES_FPGA (8U << 16)

// This is appropriate for a Verilator chip simulation with 15 min timeout
#define TRANSFER_BYTES_VERILATOR 0x2400U

// For top-level DV simulation (regression runs, deterministic behavior)
#define TRANSFER_BYTES_DVSIM 0x800U

/**
 * Indexed by usb_testutils_transfer_type_t
 */
static const char *xfr_name[] = {
    "Control",
    "Isochronous",
    "Bulk",
    "Interrupt",
};

// The transfer types of the streams
//
// Note: because Isochronous and Interrupt streams are guaranteed the requested
// amount of bus bandwidth, the number of concurrent streams of these types is
// limited to four if connected through a hub (six without hub(s)).
static const usb_testutils_transfer_type_t xfr_types[USBUTILS_STREAMS_MAX] = {
    kUsbTransferTypeIsochronous, kUsbTransferTypeBulk,
    kUsbTransferTypeBulk,        kUsbTransferTypeBulk,

    kUsbTransferTypeIsochronous, kUsbTransferTypeInterrupt,
    kUsbTransferTypeBulk,        kUsbTransferTypeBulk,

    kUsbTransferTypeInterrupt,   kUsbTransferTypeBulk,
    kUsbTransferTypeBulk,
};

// Total length of the configuration descriptor.
#define CFG_DSCR_TOTAL_LEN \
  (USB_CFG_DSCR_LEN +      \
   NUM_STREAMS * (USB_INTERFACE_DSCR_LEN + 2 * USB_EP_DSCR_LEN))

/**
 * Configuration values for USB.
 */
static uint8_t config_descriptors[CFG_DSCR_TOTAL_LEN] = {
    USB_CFG_DSCR_HEAD(CFG_DSCR_TOTAL_LEN, NUM_STREAMS)
    // Followed by programmatically-generated list of interface descriptors
};

/**
 * Test flags specifying the nature and direction of the data stream(s)
 */
static usbdev_stream_flags_t test_flags;

/**
 * Test descriptor
 */
static uint8_t test_descriptor[USB_TESTUTILS_TEST_DSCR_LEN];

/**
 * USB device context types.
 */
static usb_testutils_ctx_t usbdev;
static usb_testutils_controlep_ctx_t usbdev_control;

/**
 * Pinmux handle
 */
static dif_pinmux_t pinmux;

/**
 * State information for streaming data test
 */
static usb_testutils_streams_ctx_t stream_test;

/**
 * Specify whether to perform verbose logging, for visibility
 *   (Note that this substantially alters the timing of interactions with the
 * DPI model and will increase the simulation time)
 */
static const bool kVerbose = false;

/*
 * These switches may be modified manually to experimentally measure the
 * performance of IN traffic in isolation, or IN/OUT together etc.
 *
 * Are we sending data?
 */
static const bool kSending = true;

/**
 * Are we generating a valid byte sequence?
 */
static const bool kGenerating = true;

/**
 * Do we want the host to retry transmissions? (DPI model only; we cannot
 * instruct a physical host to fake delivery failure/packet corruption etc)
 */
static const bool kRetrying = true;

/**
 * Are we expecting to receive data?
 */
static const bool kRecving = true;

/**
 * Send only maximal length packets?
 * (important for performance measurements on the USB, but obviously undesirable
 *  for testing reliability/function)
 */
static const bool kMaxPackets = false;

/**
 * Number of streams to be created
 */
static const unsigned nstreams = NUM_STREAMS;

OTTF_DEFINE_TEST_CONFIG();

bool test_main(void) {
  // Context state for streaming test
  usb_testutils_streams_ctx_t *ctx = &stream_test;

  LOG_INFO("Running USBDEV MIXED Test");

  // Check we can support the requested number of streams
  CHECK(nstreams && nstreams < USBDEV_NUM_ENDPOINTS);

  // Decide upon the number of bytes to be transferred for the entire test
  uint32_t transfer_bytes = TRANSFER_BYTES_FPGA;
  switch (kDeviceType) {
    case kDeviceSimVerilator:
      transfer_bytes = TRANSFER_BYTES_VERILATOR;
      break;
    case kDeviceSimDV:
      transfer_bytes = TRANSFER_BYTES_DVSIM;
      break;
    case kDeviceSilicon:
      transfer_bytes = TRANSFER_BYTES_SILICON;
      break;
    case kDeviceFpgaCw340:
      break;
    default:
      CHECK(kDeviceType == kDeviceFpgaCw310);
      break;
  }
  transfer_bytes = (transfer_bytes + nstreams - 1) / nstreams;
  LOG_INFO(" - %u stream(s), 0x%x bytes each", nstreams, transfer_bytes);

  CHECK_DIF_OK(dif_pinmux_init(
      mmio_region_from_addr(TOP_EARLGREY_PINMUX_AON_BASE_ADDR), &pinmux));
  pinmux_testutils_init(&pinmux);
  CHECK_DIF_OK(dif_pinmux_input_select(
      &pinmux, kTopEarlgreyPinmuxPeripheralInUsbdevSense,
      kTopEarlgreyPinmuxInselIoc7));

  // Construct the test/stream flags to be used
  test_flags = (kSending ? kUsbdevStreamFlagRetrieve : 0U) |
               (kGenerating ? kUsbdevStreamFlagCheck : 0U) |
               (kRetrying ? kUsbdevStreamFlagRetry : 0U) |
               (kRecving ? kUsbdevStreamFlagSend : 0U) |
               // Note: the 'max packets' test flag is not required by the DPI
               (kMaxPackets ? kUsbdevStreamFlagMaxPackets : 0U);

  // Remember context state for usb_testutils context
  ctx->usbdev = &usbdev;

  // Call `usbdev_init` here so that DPI will not start until the
  // simulation has finished all of the printing, which takes a while
  // if `--trace` was passed in.
  CHECK_STATUS_OK(usb_testutils_init(ctx->usbdev, /*pinflip=*/false,
                                     /*en_diff_rcvr=*/false,
                                     /*tx_use_d_se0=*/false));

  // Initialize the state of each of the streams in turn;
  // we do this before setting up the control endpoint so that we can log the
  // configuration of each stream before the DPI model is activated by
  // enabling the interface
  uint32_t mixed_types = 0U;
  uint8_t *cfg = &config_descriptors[USB_CFG_DSCR_LEN];
  for (uint8_t s = 0U; s < nstreams; s++) {
    usb_testutils_transfer_type_t xfr_type = xfr_types[s];

    // Indicate to the DPI model the transfer type of each stream in turn.
    mixed_types |= xfr_type << (s * 2U);

    // TODO: we shall also want some Control endpoints at some point, not just
    // Endpoint Zero, but this requires more thought.
    // TODO: some of the streams should probably be undirectional, and it could
    // be a good idea to cross endpoints for a few of the streams, just in case.
    uint8_t ep_in = (uint8_t)(s + 1U);
    uint8_t ep_out = (uint8_t)(s + 1U);

    // Isochronous endpoints require a bInterval value of 1.
    uint8_t bInterval = (xfr_type == kUsbTransferTypeIsochronous ||
                         xfr_type == kUsbTransferTypeInterrupt);

    // Description of a single interface
    uint8_t int_dscr[USB_INTERFACE_DSCR_LEN + 2 * USB_EP_DSCR_LEN] = {
        VEND_INTERFACE_DSCR(s, 2, 0x50, 1),
        USB_EP_DSCR(0, ep_out, (uint8_t)xfr_type, USBDEV_MAX_PACKET_SIZE,
                    bInterval),
        USB_EP_DSCR(1, ep_in, (uint8_t)xfr_type, USBDEV_MAX_PACKET_SIZE,
                    bInterval),
    };
    // Append interface descriptor to the configuration descriptor.
    memcpy(cfg, int_dscr, sizeof(int_dscr));
    cfg += sizeof(int_dscr);

    CHECK_STATUS_OK(usb_testutils_stream_init(
        ctx, s, xfr_type, ep_in, ep_out, transfer_bytes, test_flags, kVerbose));
    LOG_INFO("S%u: IN %u:OUT %u : %s - 0x%x bytes flags 0x%x", s, ep_in, ep_out,
             xfr_name[xfr_type], transfer_bytes, test_flags);
  }

  // Inform the testutils layer of the total number of streams
  CHECK(usb_testutils_streams_count_set(ctx, nstreams));

  // Initialize the test descriptor
  // Note: the 'max packets' test flag is not required by the DPI model
  const uint8_t desc[] = {USB_TESTUTILS_TEST_DSCR(
      kUsbTestNumberMixed, NUM_STREAMS | (uint8_t)test_flags,
      (uint8_t)mixed_types, (uint8_t)(mixed_types >> 8),
      (uint8_t)(mixed_types >> 16))};
  memcpy(test_descriptor, desc, sizeof(test_descriptor));

  // This also ctivates the DPI model by asserting the pull up, indicating
  // device presence
  CHECK_STATUS_OK(usb_testutils_controlep_init(
      &usbdev_control, ctx->usbdev, 0, config_descriptors,
      sizeof(config_descriptors), test_descriptor, sizeof(test_descriptor)));

  // Proceed only when the device has been configured; this allows host-side
  // software to establish communication.
  CHECK_STATUS_OK(
      usb_testutils_controlep_config_wait(&usbdev_control, &usbdev));

  USBUTILS_USER_PROMPT("Start host-side streaming software");

  // Streaming loop; most of the work is done by the usb_testutils_streams base
  //   code and we don't need to specialize its behavior for this test.
  bool done = false;
  do {
    CHECK_STATUS_OK(usb_testutils_streams_service(ctx));

    // See whether any streams still have more work to do
    done = usb_testutils_streams_completed(ctx);
  } while (!done);

  // Determine the total counts of bytes sent and received
  uint32_t tx_bytes = 0U;
  uint32_t rx_bytes = 0U;
  for (uint8_t s = 0U; s < nstreams; s++) {
    uint32_t tx, rx;
    CHECK_STATUS_OK(usb_testutils_stream_status(ctx, s, NULL, &tx, &rx));
    tx_bytes += tx;
    rx_bytes += rx;
  }

  LOG_INFO("USB sent 0x%x byte(s), received and checked 0x%x byte(s)", tx_bytes,
           rx_bytes);

  // Note: since some streams are Isochronous, packet dropping can lead to the
  // byte count exceeding expectations.
  if (kSending) {
    CHECK(tx_bytes >= nstreams * transfer_bytes,
          "Unexpected count of byte(s) sent to USB host");
  }
  if (kRecving) {
    CHECK(rx_bytes >= nstreams * transfer_bytes,
          "Unexpected count of byte(s) received from USB host");
  }

  return true;
}
